WebApplicationContext 中特殊的 bean 类型(三)--- MultiPartResolver

MultipartResolver 是 Spring MVC 中负责处理文件的类型,使 Spring 可以支持通过 HTML 表单等格式的文件上传功能。本章将通过 MultipartResolver 源码,来解释该类型如何将上传文件请求加工为 Controller 可以认识的请求并且完成文件的存储的。最后也会按照惯例,给出对应的测试代码。测试代码将会基于上一篇的代码继续进行下去。

MultipartResolver 源码解析

MultipartResolver 接口

MultipartResolver 接口代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
package org.springframework.web.multipart;

import javax.servlet.http.HttpServletRequest;

public interface MultipartResolver {
boolean isMultipart(HttpServletRequest var1);

MultipartHttpServletRequest resolveMultipart(HttpServletRequest var1) throws MultipartException;

void cleanupMultipart(MultipartHttpServletRequest var1);
}

根据 Spring 官方文档:

By default, no multipart handling will be done by Spring, as some developers will want to handle multiparts themselves. You will have to enable it yourself by adding a multipart resolver to the web application’s context. After you have done that, each request will be inspected to see if it contains a multipart. If no multipart is found, the request will continue as expected. However, if a multipart is found in the request, the MultipartResolver that has been declared in your context will be used. After that, the multipart attribute in your request will be treated like any other attribute.

大意就是,如果在 Spring 工程中配置了 MultipartResolver, 则其会先于 HandlerMapping 转发功能生效,判断其是不是含有 Multipart 部分。具体来讲,首先截取 HttpServletRequest 调用 isMultipart 方法去判定该请求是不是存在 MultipartContent。如果有则 MultipartResolver 就会生效并且调用 resolveMultipart 将该 request 包装成为 MultipartHttpServletRequest 类供后续的 Controller 使用。

CommonsMultipartResolver 类

Spring 中 MultipartResolver 的默认实现类是 CommonsMultipartResolver,我们就从这个类入手,解释 MultiPartResolver 是如何是如何解析请求中的 Multipart 部分。首先看 CommonsMultipartResolver 对于 resolveMultipart 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
if (this.resolveLazily) {
return new DefaultMultipartHttpServletRequest(request) {
protected void initializeMultipart() {
MultipartParsingResult parsingResult = CommonsMultipartResolver.this.parseRequest(request);
this.setMultipartFiles(parsingResult.getMultipartFiles());
this.setMultipartParameters(parsingResult.getMultipartParameters());
this.setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
}
};
} else {
MultipartParsingResult parsingResult = this.parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(), parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}
}

我们可以后面再来讨论 resolveLazily 的选项,假设没有 resolveLazily,则该方法主要会调用 parseRequest 方法,再看一下 parseRequest 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = this.determineEncoding(request);
FileUpload fileUpload = this.prepareFileUpload(encoding);

try {
List<FileItem> fileItems = ((ServletFileUpload)fileUpload).parseRequest(request);
return this.parseFileItems(fileItems, encoding);
} catch (SizeLimitExceededException var5) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), var5);
} catch (FileSizeLimitExceededException var6) {
throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), var6);
} catch (FileUploadException var7) {
throw new MultipartException("Failed to parse multipart servlet request", var7);
}
}

这里主要做的是生成一个 FileUpload 类,这是一个文件上传的辅助类,并且把 request 中的 Multpart 部分解析成为 fileItem 的 List,后续就是使用 parseFileItem 将获取的 List 的每一部分进行解析(这部分感兴趣的可以再深入看下去)并且生成 CommonsMultipartFile 存入 MultipartMaps 中,我对这部分的理解就是把文件的各个部分用后续 Controller 可以识别的形式封装起来。这个部分返回的是 MultipartParsingResult,之后会被封装成为 DefaultMultipartHttpServletRequest 供后续的 Controller 解析使用。

ResolveLazily 选项

ResolveLazily 选项返回的是一个已经实现了 initializeMultipart 方法的 DefaultMultipartHttpServletRequest 对象,这会减少 MultipartResolver 这部分的开销,而在 Controller 收到该 request 并强制转义成为 MultipartRequest 时生效。可以看到,这个方法封装了 request 和对应的转换文件的方法,和我们在 ResolveLazily 为 false 时在做的事情差不多,只不过 ResolveLazily 把这些方法都封装在了 init 方法中,只有在需要调用的时候才会调用。(考虑一下你的 Controller 不用解析文件的情况)。

代码测试

为了完成文件上传功能,我们需要在 pom.xml 中配置如下的依赖:

1
2
3
4
5
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3</version>
</dependency>

之后,我们首先在 root-context.xml 中配置 MultipartResolver bean:

1
2
3
4
5
6
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="102400"></property>
<property name="maxInMemorySize" value="514" />
<property name="defaultEncoding" value="UTF-8" />
<property name="uploadTempDir" value="upload/temp" />
</bean>

uploadTempDir 表示文件在服务器缓存的地方。然后,我们配置一下文件上传的 Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
public class UploadController {

@RequestMapping(value="file-upload.do", method = RequestMethod.GET)
public String uploadPage() {
return "fileUpload";
}

@RequestMapping(value="fileUpload.do", method = RequestMethod.POST)
public void fileUpload(@RequestPart(value = "file") MultipartFile file, HttpServletRequest request) throws IOException {
String name = file.getName();

String originalFilename = file.getOriginalFilename();

String contentType = file.getContentType();

System.out.println("name -> " + name +
" originalFilename -> " + originalFilename +
" contentType -> " + contentType);
String filePath = request.getServletContext().getRealPath("/");
file.transferTo(new File(filePath + "/" + originalFilename));
}

}

上文的 MultipartFile 也是师出有名,这是由 MultipartResolver 在解析了 Request 的 MultiContent 之后才会出现的类。方法中获取了该 MultipartFile 的 key 名字,文件名,以及 contentType,一般会获得后缀名。最后,将他存到我们的 target 文件夹下面的应用运行的根目录下面。

最后,我们配置好前端页面,之前我们的 Controller 上面返回 jsp 的名字为 fileUpload,那么我们的前端页面文件名也为 fileUpload.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>file upload test</title>
</head>
<body>
<h1>File Upload Page</h1>
<form method = "post" action = "/fileUpload.do" enctype = "multipart/form-data">

<input type = "file" name = "file" /><br>

<input type = "submit" value = "开始上传"/>

</form>
</body>
</html>

最后,我们输入 http://localhost:8080/file-upload.do 之后,并且上传文件,点击“开始上传”,我们就可以看到上传的文件会存到自己的目标路径下。